昨天介紹了可選型別 (Optional),今天就針對可選鏈 (Optional Chaining),來加以介紹,目前對於可選鏈還沒有深刻的體會,就目前的理解,大概知道是用此方法可以代替昨天所述的強制解包,因強制解包,在開發者不確定是否肯定有值得情況下,容易造成運行時的錯誤,則可以用可選鏈作為替代方案,還有其他情況下的介紹,先在此紀錄,以便之後運用時可方便查詢。
可選鏈是一個查詢和調用當前可能為 nil
的可選型別之屬性、方法和下標的過程。如果可選型別包含值,則屬性、方法或下標調用成功;如果可選值為 nil
,則屬性、方法或下標調用將返回 nil
。可以將多個查詢鏈接在一起,如果鏈中的任何鏈為 nil
,則整個鏈會優雅的失敗。
Swift 中的可選鏈類似於 Objective-C 中的
nil
消息傳遞,但是它適用於任何類型,並且可以檢查成功或失敗。
可以通過在要調用其屬性、方法或下標的可選值(如果可選值不為 nil
)之後放置問號 (?) 來指定可選鏈。這與在可選值之後放置驚嘆號 (!) 以強制展開其值非常相似。主要差別在於,當可選值為 nil
時,可選鏈接會優雅的失敗,而當可選值為 nil
時,強制解包會觸發運行時錯誤。
為了反映可以在 nil
值上調用可選鏈的事實,即使要查詢的屬性、方法或下標返回的是非可選值,可選鏈調用的結果也始終是可選值。可以使用此可選返回值來檢查可選鏈調用是否成功(返回的可選包含一個值),或者由於鏈中的值為 nil
而失敗(返回的可選值為 nil
)。
具體來說,可選鏈調用的結果與預期返回值的型別相同,但包裝在可選型別中。通過可選鏈訪問時,返回 Int 屬性將返回 Int?。
接下來的幾段代碼展示了可選鏈與強制解包如何不同,並能夠檢查是否成功。
首先,定義了兩個類,分別是 Person
和 Residence
:
class Person {
var residence: Residence?
}
class Residence {
var numberOfRooms = 1
}
Residence
實例具有一個名為 numberOfRooms
的 Int 屬性,預設值為 1。Person
實例具有型別為 Residence?
的屬性可選型別 Residence
。
如果創建新的 Person
實例,則其屬性 residence
因為是可選型別而預設初始化為 nil
。在下面的代碼中,john
的屬性 residence
值為 nil
:
let john = Person()
如果嘗試訪問此人的住所的屬性 numberOfRooms
,方法是在住所後放置一個驚嘆號以強制其值解開,從而觸發運行時錯誤,因為沒有 residence
值可以解開:
let roomCount = john.residence!.numberOfRooms
// this triggers a runtime error
上述的代碼在 john.residence
具有非 nil
值時成功,並將 roomCount
設置為包含適當房間數的 Int 值。但是,如上所述,當 residence
為 nil
時,此代碼始終會觸發運行時錯誤。
可選鏈提供了一種訪問 numberOfRooms
值的替代方法。要使用可選鏈,請使用問號代替驚嘆號:
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// Prints "Unable to retrieve the number of rooms."
這告訴 Swift 可以在屬性可選型別 Residence
上「鏈接」,並且如果 residence
存在則取回 numberOfRooms
的值。
由於嘗試訪問 numberOfRooms
可能會失敗,因此可選鏈嘗試返回 Int? 型別的值,或 “optional Int” 型別。如上例所示,當 Residence
為 nil
時,此可選的 Int
也將為 nil
,以反映無法訪問 numberOfRooms
的事實。通過可選綁定以訪問可選 Int 可以解開整數並將非可選的值分配給 roomCount
變數。
請注意,即使 numberOfRooms
是非可選 Int,也是如此。通過可選鏈查詢它的事實意味著對 numberOfRooms
的調用將始終返回 Int? 而不是 Int。
可以將 Residence
實例分配給john.residence
,使其不再具有 nil
值:
john.residence = Residence()
john.residence
現在包含一個實際的 Residence
實例,而不是 nil
。如果嘗試使用與以前相同的可選鏈來訪問 numberOfRooms
,現在它將返回一個 Int? 包含預設的 numberOfRooms
值 1:
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// Prints "John's residence has 1 room(s)."
可將可選鏈與深度超過一層的屬性、方法和下標一起使用。這使我們可以深入相互關聯型別的複雜模型中的子屬性,並檢查是否可以訪問這些子屬性上的屬性、方法和下標。
下面的代碼定義了四個模型類,可用於後續的幾個範例中,包括多級可選鏈的範例。這些類透過添加帶有相關屬性、方法和下標的類 Room
和 Address
,從上面擴展了 Person
和 Residence
模型。
類 Person
的定義方式與以前相同:
class Person {
var residence: Residence?
}
類Residence
比以前複雜得多。這次,類 Residence
定義了一個名為 Rooms
的變數屬性,該屬性使用 [Room]
型別的空數組初始化:
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript(i: Int) -> Room {
get {
return rooms[i]
}
set {
rooms[i] = newValue
}
}
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
var address: Address?
}
因為此版本的 Residence
儲存了一系列 Room
實例,所以其屬性 numberOfRooms
被實現為計算屬性,而不是儲存屬性。計算出的屬性 numberOfRooms
只是從數組 rooms
中返回屬性 count
的值。
作為訪問其數組 room
的捷徑,此版本的 Residence
提供了一個可讀寫的下標,該下標提供了對數組 room
中請求索引的訪問。
此版本的 Residence
還提供了一個名為 printNumberOfRooms
的方法,該方法僅印出該房間的房間數量。
最後,Residence
定義了一個名為 address
的可選屬性,其型別為 Address?
。此屬性的類 Address
型別在下面定義。
用於數組 rooms
的類 Room
是一個簡單的類,具有一個名為 name
的屬性,以及一個用於將該屬性設置合適的房間名稱的初始化程序:
class Room {
let name: String
init(name: String) { self.name = name }
}
此模型中的最後一類稱為 Address
。此類具有三個 String?
型別的可選屬性。前兩個屬性 buildingName
和 buildingNumber
是將特定建築物標識為地址一部分的替代方法。第三個屬性 street
用來為該地址命名街道:
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier() -> String? {
if let buildingNumber = buildingNumber, let street = street {
return "\(buildingNumber) \(street)"
} else if buildingName != nil {
return buildingName
} else {
return nil
}
}
}
類 Address
還提供了一個名為 buildingIdentifier()
的方法,該方法的返回類型為 String?
。此方法檢查地址的屬性,如果有值則返回 buildingName
,如果有值則返回與 street
串聯的 buildingNumber
,否則返回 nil
。
如可選鏈代替強制展開章節中所示,可以使用可選鏈訪問可選值的屬性,並檢查該屬性訪問是否成功。
使用上面定義的類創建一個新的 Person
實例,然後嘗試像以前一樣訪問其屬性 numberOfRooms
:
let john = Person()
if let roomCount = john.residence?.numberOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// Prints "Unable to retrieve the number of rooms."
由於 john.residence
為 nil
,因此此可選的鏈調用以與以前相同的方式失敗。
還可以嘗試通過可選的鏈設置屬性的值:
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress
在此範例中,嘗試設置 john.residence
的地址屬性將失敗,因為 john.residence
當前為 nil
。
賦值是可選鏈的一部分,這意味著 "=" 運算符右側的任何代碼都不會被評估。在上一個範例中,不容易看到 someAddress
從未被評估,因為訪問常數沒有任何副作用。下面的清單執行相同的指定,但是它使用一個函數來創建地址。該函數在返回值之前印出 “Function was called”,這使我們可以查看 "=" 運算符的右側是否已被求值。
func createAddress() -> Address {
print("Function was called.")
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
return someAddress
}
john.residence?.address = createAddress()
可以說未調用 createAddress()
函數,因為未印出任何內容。
可以使用可選鏈來對在可選值上的方法進行調用,並檢查該方法調用是否成功。即使該方法未定義返回值,也可以執行此操作。
類 Residence
中的 printNumberOfRooms()
方法將印出 numberOfRooms
的當前值。該方法如下:
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
此方法未指定返回類型。但是,沒有返回類型的函數和方法的隱式返回型別為 Void。這意味著它們返回值 () 或一個空的元組。
透過過可選鏈對可選值調用此方法,則該方法的返回類型將為 Void?
,而不是 Void
,因為透過可選鏈接調用時,返回值始終為可選型別。可以使用 if 語句檢查是否可以調用printNumberOfRooms()
方法,即使該方法本身並未定義返回值。將 printNumberOfRooms
調用的返回值與 nil
進行比較,以查看方法調用是否成功:
if john.residence?.printNumberOfRooms() != nil {
print("It was possible to print the number of rooms.")
} else {
print("It was not possible to print the number of rooms.")
}
// Prints "It was not possible to print the number of rooms."
如果嘗試通過可選鏈設置屬性,也是如此。上面的「通過可選鏈訪問屬性」中的範例嘗試為 john.residence
設置地址值,即使屬性 Residence
為 nil
。任何通過可選鏈設置屬性的嘗試都會返回 Void
型別的值,則可以與 nil 進行比較以查看是否成功設置了該屬性:
if (john.residence?.address = someAddress) != nil {
print("It was possible to set the address.")
} else {
print("It was not possible to set the address.")
}
// Prints "It was not possible to set the address."
可以將多個層級的可選鏈連接在一起,以深入到模型中更深的屬性、方法和下標。但是,多個層級的可選鏈不會為返回的值添加更多層級的可選性。
換一種方式描述:
因此:
下面的範例嘗試訪問 john
的屬性 Residence
的屬性 address
的屬性 street
。此處使用兩層級的可選鏈,以鏈接屬性 residence
和 address
,這兩種均為可選類型:
if let johnsStreet = john.residence?.address?.street {
print("John's street name is \(johnsStreet).")
} else {
print("Unable to retrieve the address.")
}
// Prints "Unable to retrieve the address."
john.residence
的值當前包含有效的 Residence
實例。但是,john.residence.address
的值當前為 nil
。因此,對 john.residence?.address?.street
的調用失敗。
請注意,在上面的範例中,嘗試返回屬性 street
的值。此屬性的型別為 String?
。因此,即使除了屬性的基礎可選型別之外還應用了兩層級的可選鏈,john.residence?.address?.street
的返回值也是 String?
。
如果將實際的 Address
實例設置為 john.residence.address
的值,並為該地址的屬性 street
設置了實際值,則可以通過多層可選鏈訪問屬性 street
的值:
let johnsAddress = Address()
johnsAddress.buildingName = "The Larches"
johnsAddress.street = "Laurel Street"
john.residence?.address = johnsAddress
if let johnsStreet = john.residence?.address?.street {
print("John's street name is \(johnsStreet).")
} else {
print("Unable to retrieve the address.")
}
// Prints "John's street name is Laurel Street."
在此範例中,設置 john.residence
的屬性 address
的嘗試將成功,因為 john.residence
的值當前包含有效的 Residence
實例。
前面的範例顯示如何通過可選鏈返回可選型別屬性的值。還可以使用可選鏈來調用方法返回可選型別值,並根據需要在該方法上的鏈來返回值。
下面的範例通過可選鏈調用類 Address
的方法buildingIdentifier()
。此方法返回 String? 型別的值。如上所述,在可選鏈之後,此方法調用的最終返回型別也是 String?:
if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
print("John's building identifier is \(buildingIdentifier).")
}
// Prints "John's building identifier is The Larches."
如果要對該方法的返回值執行進一步的可選鏈,請在該方法的括號後放置可選鏈問號:
if let beginsWithThe =
john.residence?.address?.buildingIdentifier()?.hasPrefix("The") {
if beginsWithThe {
print("John's building identifier begins with \"The\".")
} else {
print("John's building identifier does not begin with \"The\".")
}
}
// Prints "John's building identifier begins with "The"."
在上面的範例中,將可選鏈問號放在括號後,因為要鏈接的可選值是方法
buildingIdentifier()
的返回值,而不是方法buildingIdentifier()
本身。